/**
 * Copyright (C) 2010-2018 Gordon Fraser, Andrea Arcuri and EvoSuite
 * contributors
 *
 * This file is part of EvoSuite.
 *
 * EvoSuite is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation, either version 3.0 of the License, or
 * (at your option) any later version.
 *
 * EvoSuite is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with EvoSuite. If not, see <http://www.gnu.org/licenses/>.
 */
package org.evosuite.testcarver.capture;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.evosuite.PackageInfo;
import org.evosuite.testcarver.instrument.TransformerUtil;
import org.objectweb.asm.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.thoughtworks.xstream.XStream;

public final class CaptureLog implements Cloneable {

	//=============   static, final fields ===================================================

	private static final Logger logger = LoggerFactory.getLogger(CaptureLog.class);

	public static final Object[] NO_ARGS = new Object[0];
	public static final String OBSERVED_INIT = "<init>";
	public static final String PLAIN_INIT = CaptureLog.class.getName() + ".PLAIN";
	public static final String COLLECTION_INIT = CaptureLog.class.getName()
	        + ".COLLECTION";
	public static final String MAP_INIT = CaptureLog.class.getName() + ".MAP";
	public static final String ARRAY_INIT = CaptureLog.class.getName() + ".ARRAY";

	public static final String NOT_OBSERVED_INIT = CaptureLog.class.getName() + ".XINIT";

	public static final String END_CAPTURE_PSEUDO_METHOD = CaptureLog.class.getName()
	        + ".END_CAPTURE";
	public static final int PSEUDO_CAPTURE_ID = Integer.MAX_VALUE; // for internally created statement (PLAIN_INIT and NOT_OBSERVED_INIT)

	public static final String EMPTY_DESC = Type.getMethodDescriptor(Type.VOID_TYPE,
	                                                                 new Type[] {});
	public static final int NO_DEPENDENCY = -1;

	public static final String PUTFIELD = "PUTFIELD";
	public static final String PUTSTATIC = "PUTSTATIC";
	public static final String GETFIELD = "GETFIELD";
	public static final String GETSTATIC = "GETSTATIC";

	public static final Object RETURN_TYPE_VOID = CaptureLog.class.getName()
	        + ".RETURN_VOID";

	private static final Set<String> NOT_OBSERVED_INIT_METHODS = Collections.synchronizedSet(new LinkedHashSet<String>());
	static {
		NOT_OBSERVED_INIT_METHODS.add(NOT_OBSERVED_INIT);
		NOT_OBSERVED_INIT_METHODS.add(COLLECTION_INIT);
		NOT_OBSERVED_INIT_METHODS.add(MAP_INIT);
		NOT_OBSERVED_INIT_METHODS.add(ARRAY_INIT);
	}

	//=============   local, object fields ===================================================

	/*
	 * FIXME: the design of this class breaks OO encapsulation. 
	 * Fields are declared 'final', but their content can be accessed/changed from outside.
	 * Need re-factoring.
	 * 
	 * For example, are these lists supposed to have same length? (ie invariant)
	 */

	//--- LOG Table
	// REC_NO | OID | METHOD | PARAMS

	/*
	 * FIXME: following lists seem to be aligned
	 */

	// rec_no is implied by index
	public final List<Integer> objectIds;
	public final List<Integer> captureIds;
	public final List<String> methodNames;
	/**
	 * FIXME: this seems always containing Integer objects, representing either
	 * null or an object identifier (oid). should it be <Integer[]> ?
	 */
	public final List<Object[]> params;
	public final List<Object> returnValues;
	public final List<Boolean> isStaticCallList;
	public final List<String> descList;

	//--- OID Info Table
	// OID | INIT_REC_NO | CLASS

	/*
	 * FIXME: the following lists seem to be aligned.
	 * Would be better to have a single list, with object
	 * containing the different fields
	 */
	private final List<Integer> oids;
	private final List<Integer> oidInitRecNo;
	private final List<String> oidClassNames;
	private final List<Integer> oidFirstInits;
	private final List<Integer> oidDependencies;

	/**
	 * captureId -> field name
	 */
	private final Map<Integer, String> oidNamesOfAccessedFields;

	/**
	 * oid -> index ==> oidInitReco.get(index) + oidClassNames.get(index)
	 */
	private final Map<Integer, Integer> oidRecMapping;

	private final XStream xstream;

	/**
	 * Main constructor
	 */
	public CaptureLog() {
		this.objectIds = new ArrayList<Integer>();
		this.methodNames = new ArrayList<String>();
		this.params = new ArrayList<Object[]>();
		this.captureIds = new ArrayList<Integer>();
		this.returnValues = new ArrayList<Object>();
		this.descList = new ArrayList<String>();

		this.oidRecMapping = new LinkedHashMap<Integer, Integer>();
		this.oidInitRecNo = new ArrayList<Integer>();
		this.oidClassNames = new ArrayList<String>();
		this.oids = new ArrayList<Integer>();
		this.oidFirstInits = new ArrayList<Integer>();
		this.oidDependencies = new ArrayList<Integer>();

		this.isStaticCallList = new ArrayList<Boolean>();

		this.oidNamesOfAccessedFields = new LinkedHashMap<Integer, String>();

		this.xstream = new XStream();
	}

	public String getNameOfAccessedFields(final int captureId) {
		return oidNamesOfAccessedFields.get(captureId);
	}

	public int getDependencyOID(final int oid) {
		int index = getRecordIndex(oid);
		return oidDependencies.get(index);
	}

	public List<Integer> getTargetOIDs(final Set<String> observedClassNames) {
		final List<Integer> targetOIDs = new ArrayList<Integer>();
		final int numInfoRecs = oidClassNames.size();
		for (int i = 0; i < numInfoRecs; i++) {
			if (observedClassNames.contains(oidClassNames.get(i))) {
				targetOIDs.add(getOID(i));
			}
		}
		return targetOIDs;
	}

	public String getTypeName(final int oid) throws IllegalArgumentException {
		if (!oidRecMapping.containsKey(oid)) {
			throw new IllegalArgumentException("OID " + oid + " is not recognized");
		}
		return oidClassNames.get(getRecordIndex(oid));
	}

	public int getRecordIndex(int oid) {
		return oidRecMapping.get(oid);
	}

	public int getOID(int recordIndex) {
		if (recordIndex < 0 || recordIndex >= oids.size()) {
			throw new IllegalArgumentException("index " + recordIndex
			        + " is invalid as there are " + oids.size() + " OIDs");
		}
		return oids.get(recordIndex);
	}
	
	public List<String> getObservedClasses() {
		return oidClassNames;
	}

	public int getRecordIndexOfWhereObjectWasInitializedFirst(int oid)
	        throws IllegalArgumentException {
		if (!oidRecMapping.containsKey(oid)) {
			throw new IllegalArgumentException("OID " + oid + " is not recognized");
		}

		int pos = oidRecMapping.get(oid);
		return oidInitRecNo.get(pos);
	}

	/**
	 * FIXME: this does not make sense... it seems like oidInitRecNo contains
	 * integers that have different meaning depending on whether their are
	 * positive or not...
	 * 
	 * @param currentRecord
	 */
	private void addNewInitRec(int currentRecord) {
		// negative log rec no indicates obj construction
		this.oidInitRecNo.add(-currentRecord);
		logger.debug("InitRecNo added " + (-currentRecord));
	}

	public void updateWhereObjectWasInitializedFirst(int oid, int recordIndex)
	        throws IllegalArgumentException {
		if (!oidRecMapping.containsKey(oid)) {
			throw new IllegalArgumentException("OID " + oid + " is not recognized");
		}
		int nRec = objectIds.size();
		/*
		 * FIXME: it seems negative indexes have special meaning...
		 */
		if (-recordIndex <= -nRec || recordIndex >= nRec) {
			throw new IllegalArgumentException("New record index " + recordIndex
			        + " is invalid, as there are only " + nRec + " records");
		}

		logger.debug("Updating init of OID " + oid + " from pos="
		        + getRecordIndexOfWhereObjectWasInitializedFirst(oid) + " to pos="
		        + recordIndex);

		// Only update init record if its number is bigger than the current init record number
		// Note that record numbers indicating fist object occurrence are marked as negative number
		// For example: constructor call at record no 8 becomes -8
		final int recentInitRecord = getRecordIndexOfWhereObjectWasInitializedFirst(oid);
		if (Math.abs(recordIndex) > Math.abs(recentInitRecord)) {
			oidInitRecNo.set(oidRecMapping.get(oid), recordIndex);
		}
	}

	@Override
	public CaptureLog clone() {
		final CaptureLog log = new CaptureLog();

		log.objectIds.addAll(this.objectIds);
		log.methodNames.addAll(this.methodNames);
		log.params.addAll(this.params);
		log.captureIds.addAll(this.captureIds);
		log.returnValues.addAll(this.returnValues);
		log.descList.addAll(this.descList);

		log.oidRecMapping.putAll(this.oidRecMapping);
		log.oidInitRecNo.addAll(this.oidInitRecNo);
		log.oidClassNames.addAll(this.oidClassNames);
		log.oids.addAll(this.oids);
		log.oidNamesOfAccessedFields.putAll(this.oidNamesOfAccessedFields);
		log.isStaticCallList.addAll(this.isStaticCallList);
		log.oidDependencies.addAll(this.oidDependencies);
		log.oidFirstInits.addAll(this.oidFirstInits);

		return log;
	}

	public void clear() {
		this.objectIds.clear();
		this.methodNames.clear();
		this.params.clear();
		this.captureIds.clear();
		this.returnValues.clear();
		this.descList.clear();

		this.oidRecMapping.clear();
		this.oidInitRecNo.clear();
		this.oidClassNames.clear();
		this.oids.clear();
		this.oidFirstInits.clear();
		this.oidDependencies.clear();
		this.isStaticCallList.clear();

		this.oidNamesOfAccessedFields.clear();
	}

	private boolean updateInfoTable(final int oid, final Object receiver,
	        final boolean replace) {
		// update oid info table, if necessary
		// -> we assume that USUALLY the first record belonging to an object belongs to its instanciation
		if (this.oidRecMapping.containsKey(oid)) {
			if (replace) {
				final int logRecNo = this.objectIds.size();
				updateWhereObjectWasInitializedFirst(oid, -logRecNo);
				return true;
			} else {
				return false;
			}
		} else {
			final int logRecNo = this.objectIds.size();
			final int infoRecNo = this.oidInitRecNo.size();

			logger.debug("Adding mapping oid->index   {} -> {}", oid, infoRecNo);
			this.oidRecMapping.put(oid, infoRecNo);
			addNewInitRec(logRecNo);

			oidFirstInits.add(logRecNo);

			oidDependencies.add(NO_DEPENDENCY);

			registerObjectsClassName(receiver);

			this.oids.add(oid);

			return true;
		}
	}

	private void registerObjectsClassName(final Object receiver) {
		if (receiver instanceof Class) //this can only happen, if there is a static method call 
		{
			final Class<?> c = (Class<?>) receiver;
			this.oidClassNames.add(c.getName().replace(PackageInfo.getEvoSuitePackage()+".testcarver.wrapper.", ""));
			//.replaceFirst("\\$\\d+$", ""));
			
		} else if (this.isPlain(receiver)) {
			// we don't need fully qualified name for plain types

			// TODO: I don't understand why we would want to shorten the name if it's a primitive.
			//       It makes it more difficult later to identify the classes contained in the log.
			this.oidClassNames.add(receiver.getClass().getName());//.replaceFirst("\\$\\d+$", ""));
		//	this.oidClassNames.add(receiver.getClass().getSimpleName());//.replaceFirst("\\$\\d+$", ""));
		} else if (isProxy(receiver) || isAnonymous(receiver)) {
			// TODO what if there is more than one interface?
			final Class<?> c = receiver.getClass();
			final Class<?>[] interfaces = c.getInterfaces();
			if(interfaces.length == 0) {
				// If there are no interfaces, try superclass?
				this.oidClassNames.add(c.getSuperclass().getName());
			} else {
				this.oidClassNames.add(interfaces[0].getName());
			}
		} else {
			String name = receiver.getClass().getName().replace(PackageInfo.getEvoSuitePackage()+".testcarver.wrapper.", "");
			this.oidClassNames.add(name);//.replaceFirst("\\$\\d+$", ""));
		}
	}

	private boolean isAnonymous(final Object receiver) {
		return receiver.getClass().isAnonymousClass();
	}

	private boolean isProxy(final Object receiver) {
		return Proxy.isProxyClass(receiver.getClass());
	}

	private boolean isPlain(final Object o) {
		return //o instanceof Class   ||
		o instanceof String || o instanceof Integer || o instanceof Double
		        || o instanceof Float || o instanceof Long || o instanceof Byte
		        || o instanceof Short || o instanceof Boolean || o instanceof Character;
	}

	/**
	 * if there is an return value and the return value creation has not been
	 * logged before (may happen, if, for example, the constructor is private),
	 * save the information that the value comes from a finished method call
	 * 
	 * @param captureId
	 * @param receiver
	 * @param returnValue
	 */
	public void logEnd(final int captureId, final Object receiver,
	        final Object returnValue) {
		if (returnValue != null && returnValue != RETURN_TYPE_VOID) {
			handleReturnValue(captureId, receiver, returnValue);
		}

		this.captureIds.add(captureId);
		this.objectIds.add(System.identityHashCode(receiver));
		this.methodNames.add(END_CAPTURE_PSEUDO_METHOD);
		this.descList.add(EMPTY_DESC);
		this.params.add(NO_ARGS);
		this.returnValues.add(RETURN_TYPE_VOID);
		this.isStaticCallList.add(Boolean.FALSE);
	}

	/**
	 * Find start of method call statement (created by CaptureLog.log()) for
	 * capture id and receiver
	 * 
	 * @param captureId
	 * @param receiver
	 * @param returnValue
	 */
	private int findRecordOfMethodStart(final Object receiver, final int captureId) {
		final int oid = System.identityHashCode(receiver);

		int currentRecord = captureIds.size() - 1;

		int nestedCalls = 0;
		while (true) {
			if (this.captureIds.get(currentRecord) == captureId
			        && this.objectIds.get(currentRecord) == oid) {
				if (this.methodNames.get(currentRecord).equals(END_CAPTURE_PSEUDO_METHOD)) {
					nestedCalls++;
				} else {
					if (nestedCalls == 0) {
						break;
					} else {
						nestedCalls--;
					}
				}
			}
			currentRecord--;
		}

		return currentRecord;
	}

	private void handleReturnValue(final int captureId, final Object receiver,
	        final Object returnValue) {
		final int returnValueOID = System.identityHashCode(returnValue);

		boolean condition = !this.oidRecMapping.containsKey(returnValueOID);

		if (!condition) {

			// oid of the target object is already known so we have to check if we should determine the corresponding method call for the return value
			// and adjust its init meta data

			final int firstInitRecNo = this.oidFirstInits.get(this.oidRecMapping.get(returnValueOID));

			final String methodName = methodNames.get(firstInitRecNo);
			final boolean isObservedConstructionCaughtForThisObject = methodName.equals(OBSERVED_INIT);
			final boolean isUnObservedConstructionCaughtForThisObject = NOT_OBSERVED_INIT_METHODS.contains(methodNames.get(firstInitRecNo));
			final boolean noReturnValueHasBeenSet = RETURN_TYPE_VOID.equals(returnValues.get(firstInitRecNo));

			if (!isObservedConstructionCaughtForThisObject
			        && !isUnObservedConstructionCaughtForThisObject) {
				final int methodStartRecord = findRecordOfMethodStart(receiver, captureId);

				// did the method call appear before the object construction was performed?
				// this is important because this method call might be used to reconstruct object construction instead of
				// the constructor calls
				if (methodStartRecord < firstInitRecNo) {
					condition = noReturnValueHasBeenSet;
				} else {
					condition = false;
				}

			} else {
				condition = noReturnValueHasBeenSet;
			}
		}

		if (condition) {
			if (!isPlain(returnValue) && !(returnValue instanceof Class)) {

				final int currentRecord = findRecordOfMethodStart(receiver, captureId);

				if (this.oidRecMapping.containsKey(returnValueOID)) {
					final int infoRecNo = this.oidRecMapping.get(returnValueOID);
					final int initRecNo = getRecordIndexOfWhereObjectWasInitializedFirst(returnValueOID);
					final String method = this.methodNames.get(Math.abs(initRecNo));

					if ((!OBSERVED_INIT.equals(method) && !NOT_OBSERVED_INIT_METHODS.contains(method)))//|| currentRecord < Math.abs(initRecNo))
					{
						this.returnValues.set(currentRecord, returnValueOID); // oid as integer works here as we exclude plain values
						updateWhereObjectWasInitializedFirst(returnValueOID,
						                                     -currentRecord);
						this.oidFirstInits.set(infoRecNo, currentRecord);
					} else {
						this.returnValues.set(currentRecord, returnValueOID);
					}

				} else {
					final int infoRecNo = this.oidInitRecNo.size();
					this.oidRecMapping.put(returnValueOID, infoRecNo);
					addNewInitRec(currentRecord);
					this.oidFirstInits.add(currentRecord);

					this.returnValues.set(currentRecord, returnValueOID); // oid as integer works here as we exclude plain values

					this.registerObjectsClassName(returnValue);

					//					this.oidClassNames.add(returnValue.getClass().getName());

					this.oids.add(returnValueOID);
					this.oidDependencies.add(NO_DEPENDENCY);
				}
			}
		}
	}

	/**
	 * For example:
	 * 
	 * public class Foo { public class Bar(){} }
	 * 
	 * 
	 * 
	 * @param receiver
	 */
	private void checkIfInstanceFromInnerInstanceClass(final Object receiver) {
		if (!(receiver instanceof Class)) {
			final Class<?> receiverClass = receiver.getClass();
			final Class<?> enclosingClass = receiverClass.getEnclosingClass();
			if (enclosingClass == null) {
				// do nothing
				return;
			} else {
				if (!receiverClass.isAnonymousClass()
				        && !Modifier.isStatic(receiverClass.getModifiers())) {
					try {
						/*
						 * The bytecode of the Outer$Inner class will contain a package-scoped field named this$0 of type Outer. 
						 * That's how non-static inner classes are implemented in Java, because at bytecode level there is no concept of an inner class.
						 *
						 * see http://stackoverflow.com/questions/763543/in-java-how-do-i-access-the-outer-class-when-im-not-in-the-inner-class
						 * for further details
						 */
						final Field this$0 = receiverClass.getDeclaredField("this$0");
						this$0.setAccessible(true);
						final Object outerInstance = this$0.get(receiver);

						//if (TransformerUtil.isClassConsideredForInstrumentation(outerInstance.getClass().getName())) {
							// FIXME
						//}

						// the enclosing object has to be restored first

						final int receiverOID = System.identityHashCode(receiver);
						final int initRecNo = this.oidRecMapping.get(receiverOID);
						this.oidDependencies.set(initRecNo,
						                         System.identityHashCode(outerInstance));
					} catch (final Exception e) {
						logger.info("An error occurred while obtaining the enclosing object of an inner non-static class instance. "
						                    + "FIELDS: "
						                    + Arrays.toString(receiverClass.getDeclaredFields()),
						            e);
					}
				}
			}
		}

	}

	public void log(final int captureId, final Object receiver, final String methodName,
	        final String methodDesc, Object... methodParams) {
		final int oid = System.identityHashCode(receiver);

		final boolean isConstructor = OBSERVED_INIT.equals(methodName);

		// TODO find nicer way
		if (PUTSTATIC.equals(methodName) || PUTFIELD.equals(methodName)) {
			/*
			 * The first param always specifies the name of the accessed field.
			 * The second param represents the actual value.
			 */
			this.oidNamesOfAccessedFields.put(captureId, (String) methodParams[0]);
			final Object assignedValue = methodParams[1];
			methodParams = new Object[1];
			methodParams[0] = assignedValue;
		} else if (GETSTATIC.equals(methodName) || GETFIELD.equals(methodName)) {
			/*
			 * The param always specifies the name of the accessed field.
			 */
			this.oidNamesOfAccessedFields.put(captureId, (String) methodParams[0]);
			methodParams = new Object[0];
		} else {
			// if it's not a constructor call, check if something regarding the receiver object has been logged before.
			// if this is not the case, we know that the object construction could not be observed. Due to the instrumentation
			// logic, this is most likely an error but we have to provide some information regarding the object construction nevertheless
			// --> create UNOBSERVED_INIT log entry
			if (!isConstructor && !this.oidRecMapping.containsKey(oid)
			        && !(receiver instanceof Class)) {
				logger.info("method {} was called on object {} with oid {} without foregoing (observed) init stmt --> creating unobserved init stmt",
				            new Object[] { methodName, receiver, oid });
				this.updateInfoTable(oid, receiver, isConstructor);
				logUnobservedInitStmt(receiver);
			}
		}

		// TODO this.updateInfoTable(oid, receiver, isConstructor);	

		// save receiver class -> might be reference in later calls e.g. doSth(Person.class)
		if (receiver instanceof Class) {
			this.objectIds.add(oid);
			this.descList.add(EMPTY_DESC);
			this.methodNames.add(PLAIN_INIT);
			this.params.add(new Object[] { receiver });
			this.returnValues.add(RETURN_TYPE_VOID);
			this.captureIds.add(PSEUDO_CAPTURE_ID);
			this.isStaticCallList.add(Boolean.FALSE);
			this.logEnd(PSEUDO_CAPTURE_ID, receiver, RETURN_TYPE_VOID);
		}

		//--- handle method params
		Object param;
		int paramOID;
		for (int i = 0; i < methodParams.length; i++) {
			param = methodParams[i];

			// null and plain params have PLAIN init stmts such as
			// Integer var0 = 122
			// Float var1 = 2.3
			// String var2 = "Hello World"
			// e.g. o.myMethod(null, var0, var1, var2);
			if (param != null) {
				// we assume that all classes (besides java and sun classes) are instrumented.
				// So if there is no foregoing entry in the oid info table, the param is a new and
				// not monitored instance. That's why this param has to be serialized.
				paramOID = System.identityHashCode(param);

				if (paramOID == oid) {
					logger.info("PARAM is 'this' reference -> are serialized version of 'this' is created and passed as param");

					// we serialize and deserialize param in order to get a 'cloned' instance of param
					// -> this approach is not very efficient but we can always clone an object without the
					//    the need of the Cloneable interface
					try {
						String xml = xstream.toXML(param);
						param = xstream.fromXML(xml);
						paramOID = System.identityHashCode(param);

						logUnobservedInitStmt(param);
					} catch (final Exception e) {
						logger.info("an error occurred while serializing and deserializing {} -> is handled as NULL param",
						            param, e);
						continue;
					}
				} else {
					createInitLogEntries(param);
				}

				// method param  has been created before so we link to it
				// NECESSARY as the object might be modified in between
				// exemplary output in test code: 
				// Object a = new Object();
				// ...
				// o.m(a);
				methodParams[i] = paramOID;
			}
		}

		// update info table if necessary
		// in case of constructor calls, we want to remember the last one
		this.updateInfoTable(oid, receiver, isConstructor);

		//--- create method call record
		this.objectIds.add(oid);
		this.methodNames.add(methodName);
		this.descList.add(methodDesc);
		this.params.add(methodParams);
		this.returnValues.add(RETURN_TYPE_VOID);
		this.captureIds.add(captureId);
		this.isStaticCallList.add(receiver instanceof Class);

		this.checkIfInstanceFromInnerInstanceClass(receiver);
	}

	@SuppressWarnings("rawtypes")
	private void createInitLogEntries(final Object param) {
		if (param == null) {
			return;
		}

		final int paramOID = System.identityHashCode(param);
		final boolean isArray = param.getClass().isArray();
		final boolean isMap = param instanceof Map;
		final boolean isCollection = param instanceof Collection;

		if (isArray || isMap || isCollection
		        || this.updateInfoTable(paramOID, param, false)) {

			final boolean isInstrumented = TransformerUtil.isClassConsideredForInstrumentation(param.getClass().getName());

			if (isPlain(param) || param instanceof Class) {
				this.objectIds.add(paramOID);
				// exemplary output in test code: Integer number = 123;
				this.methodNames.add(PLAIN_INIT);
				this.params.add(new Object[] { param });
				this.descList.add(EMPTY_DESC);
				this.returnValues.add(RETURN_TYPE_VOID);
				this.captureIds.add(PSEUDO_CAPTURE_ID);
				this.isStaticCallList.add(Boolean.FALSE);
				this.logEnd(PSEUDO_CAPTURE_ID, param, RETURN_TYPE_VOID);

			} else if (isCollection && !isInstrumented) {

				final Collection c = (Collection) param;

				final Object[] valArray = new Object[c.size()];
				int index = 0;
				for (Object o : c) {
					if (o != null) {
						createInitLogEntries(o);
						valArray[index] = System.identityHashCode(o);
					}

					index++;
				}

				if (!this.oidRecMapping.containsKey(paramOID)) {
					this.updateInfoTable(paramOID, param, true);
				}

				this.objectIds.add(paramOID);
				this.methodNames.add(COLLECTION_INIT);
				this.params.add(valArray);
				this.descList.add(EMPTY_DESC);
				this.returnValues.add(RETURN_TYPE_VOID);
				this.captureIds.add(PSEUDO_CAPTURE_ID);
				this.isStaticCallList.add(Boolean.FALSE);
				this.logEnd(PSEUDO_CAPTURE_ID, param, RETURN_TYPE_VOID);

			} else if (isMap && !isInstrumented) {

				final Map m = (Map) param;
				final Object[] valArray = new Object[m.size() * 2];

				Map.Entry entry;
				Object v, k;
				int index = 0;
				for (Object oe : m.entrySet()) {
					entry = (Map.Entry) oe;
					k = entry.getKey();
					createInitLogEntries(k);

					valArray[index++] = System.identityHashCode(k);

					v = entry.getValue();
					if (v == null) {
						valArray[index++] = null;
					} else {
						createInitLogEntries(v);

						valArray[index++] = System.identityHashCode(v);
					}
				}

				if (!this.oidRecMapping.containsKey(paramOID)) {
					this.updateInfoTable(paramOID, param, true);
				}

				this.objectIds.add(paramOID);
				this.methodNames.add(MAP_INIT);
				this.params.add(valArray);
				this.descList.add(EMPTY_DESC);
				this.returnValues.add(RETURN_TYPE_VOID);
				this.captureIds.add(PSEUDO_CAPTURE_ID);
				this.isStaticCallList.add(Boolean.FALSE);
				this.logEnd(PSEUDO_CAPTURE_ID, param, RETURN_TYPE_VOID);

			} else if (isArray) {
				// we use Array to handle primitive and Object arrays in the same way
				final int arraySize = Array.getLength(param);

				final Object[] valArray = new Object[arraySize];

				Object o;
				for (int index = 0; index < arraySize; index++) {
					o = Array.get(param, index);
					if (o != null) {
						createInitLogEntries(o);
						valArray[index] = System.identityHashCode(o);
					}
				}

				if (!this.oidRecMapping.containsKey(paramOID)) {
					this.updateInfoTable(paramOID, param, true);
				}

				this.objectIds.add(paramOID);
				this.methodNames.add(ARRAY_INIT);
				this.params.add(valArray);
				this.descList.add(EMPTY_DESC);
				this.returnValues.add(RETURN_TYPE_VOID);
				this.captureIds.add(PSEUDO_CAPTURE_ID);
				this.isStaticCallList.add(Boolean.FALSE);
				this.logEnd(PSEUDO_CAPTURE_ID, param, RETURN_TYPE_VOID);

			} else {
				// we don't need to make a dump for instrumented classes because its state changes
				// are reproducible
				if (!isInstrumented) {
					// we always need to make a dump of objects which are not instrumented
					// because the state might have changed and we couldn't observerve it
					logUnobservedInitStmt(param);
				}
			}

		}
	}

	private void logUnobservedInitStmt(final Object subject) {
		final int subjectOID = System.identityHashCode(subject);
		if (!this.oidRecMapping.containsKey(subjectOID)) {
			this.updateInfoTable(subjectOID, subject, true);
		}

		this.objectIds.add(subjectOID);
		// create new serialization record for first emersion
		// exemplary output in test code: Person newJoe = (Person) xstream.fromXML(xml); 

		this.checkIfInstanceFromInnerInstanceClass(subject);
		this.methodNames.add(NOT_OBSERVED_INIT);

		try {
			//					this.xstream.toXML(param, sout);
			//					this.sout.flush();
			//					
			//					this.params.add(new Object[]{ this.bout.toByteArray() });
			//					
			//					this.bout.reset();
			// FIXME
			this.params.add(new Object[] { this.xstream.toXML(subject) });
		} catch (final Exception e) {
			logger.info("an error occurred while serializing param '{}' -> adding null as param instead",
			            subject, e);

			// param can not be serialized -> add null as param
			this.params.add(new Object[] { null });
		}

		this.descList.add(EMPTY_DESC);
		this.returnValues.add(RETURN_TYPE_VOID);
		this.captureIds.add(PSEUDO_CAPTURE_ID);
		this.isStaticCallList.add(Boolean.FALSE);
		this.logEnd(PSEUDO_CAPTURE_ID, subject, RETURN_TYPE_VOID);
	}

	@Override
	public String toString() {
		final String delimiter = "\t|\t";

		final StringBuilder builder = new StringBuilder(1000);

		builder.append("LOG:\n").append("-------------------------------------------------------------------").append('\n').append("RECNO").append(delimiter).append("OID").append(delimiter).append("CID").append(delimiter).append("METHOD").append(delimiter).append("PARAMS").append(delimiter).append("RETURN").append(delimiter).append("IS STATIC").append(delimiter).append("DESC").append(delimiter).append("ACCESSED FIELDS").append('\n').append("-------------------------------------------------------------------").append('\n');

		int captureId;

		final int numRecords = this.objectIds.size();
		for (int i = 0; i < numRecords; i++) {
			captureId = this.captureIds.get(i);

			builder.append(i).append(delimiter) // RECNO
			.append(this.objectIds.get(i)).append(delimiter) // OID
			.append(captureId).append(delimiter) // CID
			.append(this.methodNames.get(i)).append(delimiter) // METHOD
			.append(Arrays.toString(this.params.get(i))).append(delimiter) // PARAMS
			.append(this.returnValues.get(i)).append(delimiter) // RETURN
			.append(this.isStaticCallList.get(i)).append(delimiter) // IS STATIC
			.append(this.descList.get(i)).append(delimiter) // DESC
			.append(this.oidNamesOfAccessedFields.get(captureId)) // ACCESSED FIELDS
			.append('\n');
		}

		builder.append('\n').append('\n');

		builder.append("META INF:\n").append("-------------------------------------------------------------------").append('\n').append("OID").append(delimiter).append("INIT RECNO").append(delimiter).append("OID CLASS").append(delimiter).append("ACCESSED FIELDS").append(delimiter).append("FIRST INIT").append(delimiter).append("DEPENDENCY").append('\n').append("-------------------------------------------------------------------").append('\n');

		final int numMetaInfRecords = this.oids.size();
		for (int i = 0; i < numMetaInfRecords; i++) {
			builder.append(this.oids.get(i)).append(delimiter) // OID
			.append(this.oidInitRecNo.get(i)).append(delimiter) // INIT RECNO
			.append(this.oidClassNames.get(i)).append(delimiter) // OID CLASS 
			.append(this.oidNamesOfAccessedFields.get(i)).append(delimiter) // ACCESSED FIELDS
			.append(this.oidFirstInits.get(i)).append(delimiter) // FIRST INIT FIELDS
			.append(this.oidDependencies.get(i)) // DEPENCENCY FIELDS
			.append('\n');
		}

		return builder.toString();
	}
}
